package org.diy.traffic.sources;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.GetMethod;
import org.diy.traffic.sources.TrafficSource.TrafficSourceException;
import org.diy.traffic.updates.TrafficUpdate;
import org.diy.traffic.util.TLog;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;


public class YahooTrafficSource extends TrafficSource {
	
	/**
	 * This is a identifier unique to this TrafficSource subclass which should be prepended to
	 * any individual instance names when building a unique identifier string.
	 */
	private static String sourceType = "YAHOO_SOURCE";
	
	/**
	 * This HashMap holds a list of instances of YahooTrafficSources that have been created. myInstances maps
	 * a unique string identifier to an instance of a YahooTrafficSource. The reason for this is that it helps
	 * keep track of all the cities which are being tracked and could facilitate batch operations all of them.
	 */
	private static HashMap<String, YahooTrafficSource> myInstances = 
		new HashMap<String, YahooTrafficSource>();
	
	/**
	 * An arraylist of recent updates. This is used to compare any new updates against so that this source
	 * is not posting the same updates multiple times.
	 */
	//private ArrayList<TrafficUpdate> recentUpdates = new ArrayList<TrafficUpdate>();
	
	/**
	 * I signed up for this appID by registering with the Yahoo Developer Network
	 */
	private String appID = "9ryI5m_V34HHyDszlcxPWfEucHfzfIszftA_lAz.8VwKVw.c7L8..mvM0ZZJtAQ-";
	
	/**
	 * The minimum severity level for traffic updates which are reported, measured on a scale from 1 to 5. 
	 * Yahoo defaults to 1, if severity is not specified. You may want to tweak this value, depending on
	 * how much info you want your bot to be posting....
	 */
	public final static int DEFAULT_SEVERITY_LEVEL = 2;
	private int useSeverityLevel = DEFAULT_SEVERITY_LEVEL;
	
	/**
	 * This is the base string for the Yahoo! Traffic REST API.
	 */
	private String requestBase = "http://local.yahooapis.com/MapsService/V1/trafficData?appid=" + appID;

	/** Used to build queries to the Yahoo Traffic API **/
	private final String PARAM_CITY     = "city";
	private final String PARAM_STATE    = "state";
	private final String PARAM_SEVERITY = "severity";
	
	/** The city which this YahooTrafficSource represents **/
	private String cityName;
	
	/** The state which this YahooTrafficSource represents **/
	private String stateName;
	
	/** A map which holds any parameters used in querying the Yahoo! Traffic REST API **/
	private HashMap<String, String> parameterMap = new HashMap<String, String>();
	
	/** Check if there's already an instance for a particular city **/
	public static YahooTrafficSource getInstanceByName(String encodedAddress) {
		return myInstances.get(encodedAddress);
	}
	
	/** Creates an instance of a YahooTrafficSource for a given city, state **/
	public static YahooTrafficSource createInstance(String city, String state) {
		YahooTrafficSource ytf = new YahooTrafficSource(city, state);
		myInstances.put(ytf.getName(), ytf);
		return ytf;
	}
	
	public static YahooTrafficSource createFromConfigData(HashMap<String, String> configParams) throws TrafficSourceException {
		String city_name        = configParams.get("cityName");
		String state_name       = configParams.get("stateName");
		//String twitter_username = configParams.get("username");
		//String twitter_passwd   = configParams.get("password");
		
		
		if ((city_name== null) || (state_name==null)) {
			TLog.log(Level.SEVERE, "YahooTrafficSource requires a city name and state name. Check config file.");
			return null;
		}
		
		int use_update_interval = DEFAULT_UPDATE_FREQUENCY_S;
		
		String updateInterval = configParams.get("updateInterval");
		if (updateInterval != null) {
			use_update_interval = Integer.parseInt(updateInterval);
		} 
		
		return new YahooTrafficSource(city_name, state_name, use_update_interval);
	}
	
	/**
	 * Constructor for a Yahoo traffic source. If you're concerned with making sure there's only one YTS for
	 * each city, use the factory method YahooTrafficSource.getInstace(); otherwise, this method can be called
	 * directly.
	 * @param city   The city which this YahooTrafficSource represents
	 * @param state  The state which this YahooTrafficSource represents
	 */
	public YahooTrafficSource(String city, String state) {
		// this constructor does not take a update frequency, so use the default
		this(city, state, TrafficSource.DEFAULT_UPDATE_FREQUENCY_S); //900 seconds is 15 minutes;
	}
	
	/**
	 * Constructor for YahooTrafficSource which takes city, state and updateFrequency
	 * @param city		The city which this YahooTrafficSource represents
	 * @param state		The state which this YahooTrafficSource represents
	 * @param updateFrequency A value in seconds which represents how often this source should be updated
	 */
	public YahooTrafficSource(String city, String state, int updateFrequency) {
		super(TrafficSource.encodeAddress(new String[]{city,state}), updateFrequency);
		this.cityName  = city;
		this.stateName = state;
		// create this source's unique name by appending the address info to the base sourcetype
		this.sourceName = YahooTrafficSource.sourceType + "_" + TrafficSource.encodeAddress(new String[]{city,state});
	}
	
	
	public YahooTrafficSource(HashMap<String, String> configParams) {
		this(configParams.get("cityName"), 
				configParams.get("stateCode"), 
				Integer.parseInt(configParams.get("updateFrequency"))
				);
	}
	
	
	/**
	 * Helper function which takes a map of keys and values and puts them into this instance's parameter map
	 * @param m A map of keys (String) to values (String) representing parameters to be passed into the Yahoo Traffic REST API query
	 */
	public void configureParameters(Map<String,String> m) {
		Set<String> keys = m.keySet();
		Iterator<String> key_iterator = keys.iterator();
		while (key_iterator.hasNext()) {
			String next_key = key_iterator.next();
			String next_val = m.get(next_key);
			parameterMap.put(next_key, next_val); // not URIencoded yet!		
		}
	}
	
	public int getSeverityLevel() {
		return useSeverityLevel;
	}
	public void setSeverityLevel(int l) {
		useSeverityLevel = l;
	}
	
	/**
	 * This function does the bulk of the work in checking Yahoo! for traffic updates.
	 * @param updateLastCheck true if this source's last check should be updated to now
	 * @return A list of TrafficUpdate objects, which could be empty if there are no updates.
	 * @throws YahooTrafficException if something goes wrong while checking for updates
	 */
	@Override
	public List<TrafficUpdate> doCheck(boolean updateLastCheck) throws YahooTrafficException {
		
		// will contain the TrafficUpdate objects, if there are any
		ArrayList<TrafficUpdate> trafficUpdates = new ArrayList<TrafficUpdate>();
		
		// build the HTTP request
		String request = requestBase;
		request += ( "&" + PARAM_CITY  + "=" + plusEncode(cityName) );
		request += ( "&" + PARAM_STATE + "=" + plusEncode(stateName) );
		request += ( "&" + PARAM_SEVERITY + "=" + useSeverityLevel );
		
		
		// add in any additional keys which were put in the parameterMap
		Set<String> keys = parameterMap.keySet();
		Iterator<String> key_iterator = keys.iterator();
		while (key_iterator.hasNext()) {
			String next_key = key_iterator.next();
			String next_val = parameterMap.get(next_key);
			request += ( "&" + next_key + "=" + plusEncode(next_val));			
		}
		
		//TLog.log(Level.FINE, "Here the request is " + request);
		
		// Create a new GET with the built request
        HttpClient client = new HttpClient();
        GetMethod method = new GetMethod(request);
        
        // Send GET request
        int statusCode;
        try {
        	statusCode = client.executeMethod(method);
        } catch (HttpException he) {
        	throw new YahooTrafficException("Error executing \"GET\": " + he.getMessage());
        } catch (IOException ioe) {
        	throw new YahooTrafficException("Error executing \"GET\": " + ioe.getMessage());
        }
        
        if (statusCode != HttpStatus.SC_OK) {
        	// if something has gone wrong ....
        	TLog.log(Level.SEVERE, "Method failed: " + method.getStatusLine());
        	throw new YahooTrafficException("Error fetching Yahoo Traffic page");
        }
        
        InputStream rstream = null;
        
        // Get the response body
        try {
        	rstream = method.getResponseBodyAsStream();
        } catch (IOException ioe) {
        	throw new YahooTrafficException("Error getting \"GET\" response body: " + ioe.getMessage());
        }
        	
        // Process response
        Document response;
        try {
        	response = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(rstream);
        } catch (Exception e){
        	throw new YahooTrafficException("Exception [" + e.getClass()+ "] while building XML doc: " + e.getMessage());
        } 
        
        // A bunch of XML parsing....
        XPathFactory factory = XPathFactory.newInstance();
        XPath xPath=factory.newXPath();
        
        //Get all search Result nodes
        
        NodeList nodes;
        try {
        	// Get each <ResultSet><Result> node
        	nodes = (NodeList)xPath.evaluate("/ResultSet/Result", response, XPathConstants.NODESET);
        } catch (XPathExpressionException xpee)  {
        	try {
        		// if something went wrong getting the Results, perhaps Yahoo reported an error?
        		nodes = (NodeList) xPath.evaluate("/Error/Message", response, XPathConstants.NODESET);
        	} catch (XPathExpressionException xpee2){
        		// if you can't even find the <Error><Message> I dont know w.t.f. happened
        		throw new YahooTrafficException("Don't understand Yahoo response" + "[" + xpee.getMessage() + "]");
        	}
        	// didn't get any <Result>s most likely because it couldn't find your location
        	throw new YahooTrafficException("Can't find location " + "[" + xpee.getMessage() + "]");
        }
          
        //Iterate over search Result nodes 
        int nodeCount = nodes.getLength();
        for (int i = 0; i < nodeCount; i++) {
           
        	//Get each xpath expression as a string
        	String title, summary;
        	try {
        		// The title is a succinct representation of the traffic update, so don't worry about the description element 
        		title = (String)xPath.evaluate("Title", nodes.item(i), XPathConstants.STRING);
        		summary = (String)xPath.evaluate("Description", nodes.item(i), XPathConstants.STRING);
        	} catch (XPathExpressionException xpee) {
        		throw new YahooTrafficException("Can't find location " + "[" + xpee.getMessage() + "]");
        	}
        	
        	// Create a new TrafficUpdate and add it to the List. As arguments, it takes a reference to the source
        	// (this), a timestamp, and an actual update message, for which we use title we scraped from XML.
        	trafficUpdates.add(new TrafficUpdate(this, System.currentTimeMillis(), title ));
         }
        
        if (updateLastCheck) {
        	// if we're supposed to update, then use the utility function to set lastcheck to now.
        	setLastCheckToNow();
        }

        
        //IMPORTANT!! To make sure that the source isn't repeatedly posting the same updates, they must go
        // through a reconciliation process. This function takes care of weeding out any updates which have stayed
        // posted on the server (Yahoo in this case) which I've already posted....
        reconcileNewAndRecentUpdates(trafficUpdates);
        
        return trafficUpdates;
	}
	
	/**
	 * This function takes a list of TrafficUpdates and reconciles them with this Source's list of recent updates,
	 * to make sure that I'm not posting the same updates two times in a row, just because they're still being pulled
	 * Yahoo server. Here's how it works: If my recentUpdates contains A,B,C and the list of newUpdates from the
	 * server has B,C, and D, then after this function is run, recentUpdates will have B,C and newUpdates will have
	 * just D.  
	 * TODO Tweak algorithm for this, it's pretty naive now. Isn't there a better way to do this?
	 * @param newUpdates a list of TrafficUpdates to reconcile with my internal list of recent updates
	 */
	/**
	private void reconcileNewAndRecentUpdates(ArrayList<TrafficUpdate> newUpdates) {
		
		ArrayList<TrafficUpdate> newUpdatesClone = (ArrayList<TrafficUpdate>) newUpdates.clone();
		
		Iterator<TrafficUpdate> new_iterator = newUpdates.iterator();
		while (new_iterator.hasNext()) {
			TrafficUpdate u = new_iterator.next();
			if (recentUpdates.contains(u)) {
				// use the iterator's function to remove the last item, so I don't get a ConcurrentModificationException
				new_iterator.remove();
			} else {
				recentUpdates.add(u);
			}
		}
		
		Iterator<TrafficUpdate> recent_iterator = newUpdates.iterator();
		while (recent_iterator.hasNext()) {
			TrafficUpdate u = recent_iterator.next();
			if (!newUpdatesClone.contains(u)) {
				recentUpdates.remove(u);
			} 
		}
	}
	**/
	
	/**
	 * Encodes a String specifically to work with the Yahoo API. Substitutes "+" in place of " "
	 * @param val a string to be plus-encoded
	 * @return a string which represents val, with all " " characters replaced by "+" characters
	 */
	private String plusEncode(String val){
		return val.replace(" ", "+");
	}	
	
	/**
	 * Helper function to print out a list of TrafficUpdates
	 * @param l the list of TrafficUpdates to print.
	 */
	private void printUpdates(ArrayList<TrafficUpdate> l) {
		Iterator<TrafficUpdate> i = l.iterator();
		while (i.hasNext()) {
			TLog.log(Level.FINEST, (i.next()).toString());
		}
	}
	
	/**
	 * A class which represents an exceptional state which occurred during the processing of Yahoo! traffic updates.
	 * Inherits from TrafficSourceException class.
	 * @author Dan Greenblatt
	 *
	 */
	public class YahooTrafficException extends TrafficSourceException {
		
		public YahooTrafficException() {super();}
		
		public YahooTrafficException(Exception e) {super(e);}
		
		public YahooTrafficException(String s) {super(s);}
	}
}
